UnityのDebug.Logの話(IL2CPPでログを出したいみたいな話を添えて)


概要

個人的にUnityでのLogが実機(iOSとかIL2CPPの果て)で最終的にどうなるか、という動作を追い切ったことがないんで、追った話。



前提

・UnityEditorのLogging設定では、ログは出る。stacktraceが止むだけ。

・UnityEngine.Debug.unityLogger.logEnabled = false; では、実行時にこのコードを実行後、ログは出なくなる。


ここまででいうと、後者、 UnityEngine.Debug.unityLogger.logEnabled = false; いいじゃんってなると思うんだけど、まだ気になるポイントがあった。



発端

UnityEngine.Debug.unityLogger.logEnabled = false; を使っても、Debug.Logの中の実装が変わるだけで、

Debug.Log(“a:” + 1000);

とかの中身が変わってログは出なくなるが、


“a:” + 1000 の部分は、“a:” + 1000.ToString() みたいな処理が走る。


処理が走るやんけ、というところで、ToStringは new string ~ みたいなのがその結果として発生するので、

結果としてログ関数の中身を無効化しても、無駄なnew発生してるやんけという話。



before

Debug.Log(“a:” + 1000);


after

UnityEngine.Debug.unityLogger.logEnabled = false;

Debug.Log(“a:” + 1000);// 関数の中身がすげ変わってログは出なくなってるが、1000をstringに変える、という処理は残っている。



これはまあ、“a:” + 1000の部分で発生したToString()の成果が、GCの対象となる。



で、まあ、つまり、

・UnityEngine.Debug.unityLogger.logEnabled = false; をやっても、結局ログコード自体が残っていると無駄なnewが走る


というわけだ。俺はこの時点で結構絶望した。



なんでかというと、UnityEngine.Debug.Log類をglobalで上書きする、という、ヒューマンパワーを食ううえに不完全な方法しか解決策がないから。


どう不完全か

この辺がわかりやすい。

【Unity】リリース時にDebug.Logを出力しないようにする

https://noracle.jp/unity-no-debug/


この記事では、

・自前でUnityEngine.Debugを上書きして

・Conditional属性をつけることで、根こそぎ無効にできる

という手法と、その手法がなお及ばない部分がある、と言うことを示してくれている。


・using A=B とかやられてるやつは対処できない(ライブラリとかにめっちゃある)

ということがそれ。コードならいいけど、dllなら? 詰むが?



そしてそれとは別に、まあ、自分はこっちの方が気になるんだけど、

・毎回誰かが同じ、Debug.Log*系のコードを書かないといけない

これなんの罰ゲームだよ。被ったりするしみんなが同じことを毎回やる必要ないでしょ。


そう、つまり不完全なのだよ。

完全な、デザインされた元栓が欲しいんだ。



こうであってほしい

・コンパイル時にUnityEngine.Debug.LogをConditionalで制御できるようにしてほしい

・そしたらコンパイル時にUnityEngine.Debug.Logを呼んでる箇所は、たとえdllの中でも消えてくれると思うし、

・例えばログのためのローカル変数とかもunused扱いになってコンパイルで消せると思うので



みたいな感じ。ユーザーが書けるC#レイヤーでこういう問題に挑ませるの悪手だと思う。

具体例を書こうか。


こういうコードがあったら、

foreach (var path in deleteTargetPaths)

{

    Directory.Delete(path, true);

    Debug.Log("folder deleted:" + path);

}



もし、Debug.LogがConditionalAttributeとかで実装してあれば、

foreach (var path in deleteTargetPaths)

{

    Directory.Delete(path, true);


}


コンパイル時にこうなったりするはずなんだよ。

するとどうだ、"folder deleted:" + path が関数ごと消えるでしょ。こうなれば関数がどうなってようと関係ないじゃん。幸せでは。



IL2CPP側はどうなっているのか

で、まあ、自分の中での結論は↑の通り、UnityEngine.DebugにConditionalつけといてよ、なのだが、

それはまあなんか言う機会を作って言えばいい話なので、なんならバグレポレベルだと思ってるしそれはそれとして。


今できることはなんだろうか。


例えば、IL2CPPへとコンパイルされた段階で、Debug.Log関数の元締めは世界で一個しかないはずだ。

だから、その関数をIL2CPP上から滅殺できる、みたいなマクロがもしラッパーとして定義されていれば、


その部分をいじることでマクロとしてDebug.Logを消し、Log関数に読み込まれている各種string化処理とかconcat処理とかも、

コンパイラが「お前これつかってないやんけ!!」パワーを発揮して滅殺してくれるのでは!???!? という夢があった。



結論から書くとそれは夢だった。マクロなんかなかった。


IL2CPP世界のログの定義はこんな感じのところにある。

PROJECT/iOS/Classes/Native/Bulk_Assembly-CSharp_0.cpp


(行数に注目するとちょっとクスッとくると思う)

スクリーンショット 2019-06-21 18.10.44.png


さあ、C# UnityEngine.Debug.Logが変換されて出てくるログの関数を探そう。

該当の関数はこれ。

// System.Void UnityEngine.Debug::Log(System.Object)

extern "C" IL2CPP_METHOD_ATTR void Debug_Log_m4051431634 (RuntimeObject * __this /* static, unused */, RuntimeObject * p0, const RuntimeMethod* method);


長えな。

mなんちゃらの数値は、Unityのバージョンに関連なく固定っぽい。特にビルド対象によって変動はしなかった。



そしてこの関数はIL2CPP化されたコードの上で使われてるはずなので、使用箇所を探してみる。


Debug_Log_m4051431634(NULL /*static, unused*/, _stringLiteral223888751, /*hidden argument*/NULL);


こんな感じに、そのままログ関数がコード上で使われている。

マクロとかでラップされてて、マクロを書き換えればあら不思議、なかったことに、、、!! みたいな世界はこの世にはなかった。本当に残念だ。


ここで、「マクロで書かれてれば最悪でもIL2CPPレイヤーでなんとかできるのでは」という夢は霧散する。



IL2CPPでDebug.Logを書こう

最後に、IL2CPPで好きなようにログを書く、という、この世の果てで使えそうなデバッグ手法を紹介しておく。


IL2CPPで出力されたC++コードとは何か、っていうと、あれは、コンパイル結果だ。

断じて人間が何かを書き加えたり、変更を加えられる空間ではない。


で、まあ、


Debug_Log_m4051431634(NULL /*static, unused*/, _stringLiteral223888751, /*hidden argument*/NULL);


とかのコードでは、

_stringLiteral223888751

というポインタにIL2CPP string型みたいなのがあって、そこにC#で書いたC# stringが置かれている。

(実際にはバカでっかい文字列置き場があって、そこにすべての文言が連続する形でキュッと収められている。)



さて、そのIL2CPP string、自力で作ることはできるのか。


できる。


IL2CPP_RUNTIME_CLASS_INIT(Debug_t3317548046_il2cpp_TypeInfo_var);

String_t* a = il2cpp_codegen_string_new_wrapper("comment here!!");

Debug_Log_m4051431634(NULL, (RuntimeObject *)a, NULL);


こんなふうにすれば、comment here!! っていう表示がXcodeとかに出る。

便利でっしゃろ。


IL2CPP世界ではXcodeのbreak pointを置くことでもちろんいい感じにステップ実行できるんだけど、

ポインタのラッパーやラッパーのラッパーがあるせいで、さてここにどうやってジャンプしてきたんだろう、みたいなスタック情報がかき消えているケースが多い。


つまり上から下は見やすいんだけど、

下から上が超絶に見えない。


となると最後の手段はログでしょって感じで、上記のコードが使える。


前提としては、

#include "codegen/il2cpp-codegen.h"

をincludeしている必要があるが、IL2CPPで作成されたコードにはだいたいincludeされてる。



感想

ここまで読まれた人がいたら、ついででいいので読んでおいてほしい。


自分はこう思っている。

・Debug.Logを無効化する方法はいろいろあるが、いまだに完全なものはない。完全なものが欲しい。誰が書いたどんなコードにも適応できる、完全なものが。

・まあ別にstring concatとかToStringとか重たい関数をDebug.Log()のかっこの中で実行してGCがでるくらいいいじゃん、という発想は別にアリだと思っている。

・Debug.Logが一つでもコードに残っているのは悪だ、どんどん消すべき!みたいなのには別に同意しない。

・そんなのは書いたやつの自由だし、なんで製品版ビルドなのにログで損をするんだよという感じ。

・そして、それらとは関係なく、この文脈においてDebug.Logをユーザーに上書きさせることはやはり狂っている。


以上だ。

はい。